import Recorder from "./recordmanager/index.esm.js";
import hmacSHA256 from "crypto-js/hmac-sha256";
import Base64 from "crypto-js/enc-base64";

type Params = {
  appid: string;
  api_secret: string;
  api_key: string;
  server_url: string;
};

// 链接状态
enum ASRSTATUS {
  "CONNECTING" = "建立连接中",
  "OPEN" = "录音中",
  "CLOSING" = "关闭连接中",
  "CLOSED" = "开始语音输入",
}

// ASR 链接状态类型
type AsrStatus = keyof typeof ASRSTATUS;

export default class VoiceControl {
  private APPID: string;
  private API_SECRET: string;
  private API_KEY: string;
  private SERVER_URL: string;
  private _iatWS: WebSocket;
  private _recorder: RecorderManager;
  private _websocketUrl: string;
  private _state: AsrStatus;
  private _changeListeners: null | ((state: AsrStatus) => void);
  private _errorListener: null | ((errorMsg: any) => void);
  private _errorMsg: string;
  private _resultText: string; // 转语音后的文字
  private _resultTextListener: null | ((text: string) => void);
  private _realTimeText: string; // 实时数据
  private _realTimeTextTemp: string;
  private _resultCommandListener: null | ((text: any) => void);
  private _resultCommand: any;

  get state() {
    return this._state;
  }

  set state(newState: AsrStatus) {
    if (this._state !== newState) {
      this._state = newState;
      this._changeListeners!(this._state);
    }
  }

  get errorMsg() {
    return this._errorMsg;
  }

  set errorMsg(errorMsg: any) {
    this._errorMsg = errorMsg;
    this._errorListener!(this._errorMsg);
  }

  get resultText() {
    return this._resultText;
  }

  set resultText(text: string) {
    this._resultText = text;
    this._resultTextListener!(this._resultText);
  }

  get resultCommand() {
    return this._resultCommand;
  }

  set resultCommand(command: any) {
    this._resultCommand = command;
    this._resultCommandListener!(this._resultCommand);
  }

  constructor(params: Params) {
    this.prepare(params);
    this.init();
  }

  private prepare(params: Params) {
    // 参数校验
    this.APPID = params.appid;
    this.API_SECRET = params.api_secret;
    this.API_KEY = params.api_key;
    this.SERVER_URL = params.server_url;
  }

  private init() {
    try {
      this._changeListeners = () => {};
      this._errorListener = () => {};
      this._resultTextListener = () => {};
      this._resultText = "";
      this._recorder = new Recorder("./");
      // 初始化 WebSocketUrl
      this._websocketUrl = this.getWebSocketUrl();
      // 开始为关闭状态
      this.state = "CLOSED";
      this._recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
        if (this._iatWS.readyState === this._iatWS.OPEN) {
          this._iatWS.send(
            JSON.stringify({
              data: {
                status: isLastFrame ? 2 : 1,
                format: "audio/L16;rate=16000",
                encoding: "raw",
                audio: this.toBase64(frameBuffer),
              },
            }),
          );
          if (isLastFrame) {
            // 最后一组数据的时候，修改状态为关闭连接中
            this.changeState("CLOSING");
          }
        }
      };
      this._recorder.onStop = () => {
        this.changeState("CLOSED");
      };
    } catch (e) {
      this.errorMsg = e;
    }
  }

  onStateChange(callback: (state: AsrStatus) => void) {
    this._changeListeners = callback;
  }

  onError(callback: (e: any) => void) {
    this._errorListener = callback;
  }

  onResultTextChange(callback: (text: string) => void) {
    this._resultTextListener = callback;
  }

  onResultCommand(callback: (command: any) => void) {
    this._resultCommandListener = callback;
  }

  // 切换开启/关闭
  start() {
    this.changeState("OPEN");
  }

  // 关闭
  stop() {
    this.changeState("CLOSED");
  }

  private toBase64(buffer: ArrayBuffer) {
    let binary = "";
    const bytes = new Uint8Array(buffer);
    // let len = bytes.byteLength;
    for (let i = 0, len = bytes.byteLength; i < len; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
  }

  private getWebSocketUrl() {
    // 请求地址根据语种不同变化
    let url = "wss://iat-api.xfyun.cn/v2/iat";
    const host = "iat-api.xfyun.cn";
    const apiKey = this.API_KEY;
    const apiSecret = this.API_SECRET;
    const date = new Date().toGMTString();
    const algorithm = "hmac-sha256";
    const headers = "host date request-line";
    const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
    const signatureSha = hmacSHA256(signatureOrigin, apiSecret);
    const signature = Base64.stringify(signatureSha);
    const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
    const authorization = btoa(authorizationOrigin);
    url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
    return url;
  }

  private changeState(status: AsrStatus) {
    if (status === "CONNECTING") {
      // 开始连接的时候，清空之前的数据
      this._resultText = "";
      this._realTimeTextTemp = "";
      this._realTimeText = "";
    }
    if (status === "OPEN") {
      console.log("VoiceControl.WebSocket.onopen");
      // 开始录音，启动倒计时
      if (this.state === "CLOSED") {
        this.connectWebSocket();
      } else {
        // 结束录音
        // this._recorder.stop();
        this.changeState("CLOSED");
      }
    }
    if (status === "CLOSED") {
      // 结束录音
      this._iatWS.close();
      // console.debug("VoiceControl.WebSocket.close");
      this._recorder.stop();
      // console.debug("VoiceControl._recorder.stop");
    }
    this.state = status;
  }

  private connectWebSocket() {
    if ("WebSocket" in window) {
      this._iatWS = new WebSocket(this._websocketUrl);
    } else if ("MozWebSocket" in window) {
      /* eslint-disable-next-line no-undef */
      this._iatWS = new MozWebSocket(this._websocketUrl);
    } else {
      // alert("浏览器不支持WebSocket");
      this.errorMsg = "浏览器不支持WebSocket";
      return;
    }
    this.changeState("CONNECTING");
    this._iatWS.onopen = (e) => {
      // 开始录音
      this._recorder.start({
        sampleRate: 16000,
        frameSize: 1280,
      });
      console.log("VoiceControl._recorder.start");
      const params = {
        common: {
          app_id: this.APPID,
        },
        business: {
          language: "zh_cn",
          domain: "iat",
          accent: "mandarin",
          vad_eos: 5000,
          // dwa: "wpgs",
        },
        data: {
          status: 0,
          format: "audio/L16;rate=16000",
          encoding: "raw",
        },
      };
      this._iatWS.send(JSON.stringify(params));
    };
    this._iatWS.onmessage = (e) => {
      this.getDictationResult(e.data);
    };
    this._iatWS.onerror = (e) => {
      console.error(e);
      this.errorMsg = e;
      this.changeState("CLOSED");
    };
    this._iatWS.onclose = (e) => {
      this.changeState("CLOSED");
    };
  }

  private async getDictationResult(resultData: string) {
    // 识别结束
    const jsonData = JSON.parse(resultData);
    if (jsonData.data && jsonData.data.result) {
      const data = jsonData.data.result;
      let str = "";
      const ws = data.ws;
      for (let i = 0; i < ws.length; i++) {
        str = str + ws[i].cw[0].w;
      }
      // 开启wpgs会有此字段(前提：在控制台开通动态修正功能)
      // 取值为 "apd"时表示该片结果是追加到前面的最终结果；取值为"rpl" 时表示替换前面的部分结果，替换范围为rg字段
      if (data.pgs) {
        if (data.pgs === "apd") {
          // 将_realTimeTextTemp同步给_realTimeText
          this._realTimeText = this._realTimeTextTemp;
        }
        // 将结果存储在_realTimeTextTemp中
        this._realTimeTextTemp = this._realTimeText + str;
      } else {
        this._realTimeText = this._realTimeText + str;
      }
      this.resultText = this._realTimeTextTemp || this._realTimeText || "";
    }
    if (jsonData.code === 0 && jsonData.data.status === 2) {
      /**
       * 0：识别的第一块结果
       * 1：识别中间结果
       * 2：识别最后一块结果
       */
      this.changeState("CLOSED");
      // TODO: 请求指令模型
      const fetchRes = await this.getCommand(this._resultText);
      const res = await fetchRes!.json();
      this.resultCommand = res;
    }
    if (jsonData.code !== 0) {
      this.changeState("CLOSED");
      this.errorMsg = JSON.stringify(jsonData);
    }
  }

  private async getCommand(text: string) {
    try {
      return await fetch(`${this.SERVER_URL}?text=${text}`);
    } catch (e) {
      this.errorMsg = e;
    }
  }
}
